/*
 * Unidata Platform
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 *
 * Commercial License
 * This version of Unidata Platform is licensed commercially and is the appropriate option for the vast majority of use cases.
 *
 * Please see the Unidata Licensing page at: https://unidata-platform.com/license/
 * For clarification or additional options, please contact: info@unidata-platform.com
 * -------
 * Disclaimer:
 * -------
 * THIS SOFTWARE IS DISTRIBUTED "AS-IS" WITHOUT ANY WARRANTIES, CONDITIONS AND
 * REPRESENTATIONS WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
 * IMPLIED WARRANTIES AND CONDITIONS OF MERCHANTABILITY, MERCHANTABLE QUALITY,
 * FITNESS FOR A PARTICULAR PURPOSE, DURABILITY, NON-INFRINGEMENT, PERFORMANCE AND
 * THOSE ARISING BY STATUTE OR FROM CUSTOM OR USAGE OF TRADE OR COURSE OF DEALING.
 */
package org.unidata.mdm.rest.meta.service;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.cxf.jaxrs.ext.multipart.Attachment;
import org.apache.cxf.jaxrs.ext.multipart.Multipart;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.context.ModelChangeContext;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.model.StorageElement;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.meta.context.UpsertDataModelContext;
import org.unidata.mdm.meta.service.MetaDependencyService;
import org.unidata.mdm.meta.type.input.meta.MetaGraph;
import org.unidata.mdm.meta.type.input.meta.MetaType;
import org.unidata.mdm.meta.type.instance.DataModelInstance;
import org.unidata.mdm.meta.type.model.DataModel;
import org.unidata.mdm.rest.meta.converter.graph.MetaGraphDTOToROConverter;
import org.unidata.mdm.rest.meta.converter.graph.MetaTypeROToDTOConverter;
import org.unidata.mdm.rest.meta.exception.MetaRestExceptionIds;
import org.unidata.mdm.rest.meta.ro.MetaDependencyRequestRO;
import org.unidata.mdm.rest.meta.ro.ModelVersionRO;
import org.unidata.mdm.rest.system.ro.ErrorResponse;
import org.unidata.mdm.rest.system.ro.RestResponse;
import org.unidata.mdm.rest.system.ro.UpdateResponse;
import org.unidata.mdm.rest.system.service.AbstractRestService;
import org.unidata.mdm.system.exception.PlatformBusinessException;
import org.unidata.mdm.system.serialization.xml.XmlObjectSerializer;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;

/**
 * The Class MetaModelRestService.
 *
 * @author Mikhail Mikhailov Meta model management REST interface.
 */
@Path(MetaModelRestService.SERVICE_PATH)
@Consumes({ "application/json" })
@Produces({ "application/json" })
public class MetaModelRestService extends AbstractRestService {
    /**
     * Logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(MetaModelRestService.class);
    /**
     * Service path.
     */
    static final String SERVICE_PATH = "model";
    /**
     * Path param all.
     */
    private static final String PATH_PARAM_ALL = "all";
    /**
     * Path param dependency.
     */
    private static final String PATH_PARAM_DEPENDENCY = "dependency";
    /**
     * Path param model name.
     */
    private static final String PATH_PARAM_MODEL_NAME = "model-name";
    /**
     * File name (attachment param).
     */
    private static final String DATA_PARAM_FILE = "file";
    /**
     * Recreate he model or not.
     */
    private static final String DATA_PARAM_RECREATE = "recreate";
    /**
     * Meta model service.
     */
    @Autowired
    private MetaModelService metaModelService;
    /**
     * Meta dependency service.
     */
    @Autowired
    private MetaDependencyService metaDependencyService;
    /**
     * The name.
     */
    private String name = "DEFAULT";

    /**
     * Constructor.
     */
    public MetaModelRestService() {
        super();
    }

    /**
     * Gets entities in paged fashion with defaults.
     *
     * @return list of entities
     */
    @GET
    @Path("/" + PATH_PARAM_ALL)
    @Operation(
        description = "Список идентификаторов моделей.",
        method = HttpMethod.GET,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response findAll() {
        return ok(new RestResponse<>(metaModelService.getStorageInstance().getActive().stream()
                .map(StorageElement::getStorageId)
                .collect(Collectors.toList())));
    }

    /**
     * Model name.
     *
     * @return the response
     */
    @GET
    @Path("/" + PATH_PARAM_MODEL_NAME)
    @Operation(
        description = "Имя и версия метамодели.",
        method = HttpMethod.GET,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response modelName() {

        DataModelInstance i = metaModelService.instance(Descriptors.DATA);
        ModelVersionRO modelVersionRO = new ModelVersionRO();
        modelVersionRO.setStorageId(i.getStorageId());
        modelVersionRO.setVersion(i.getVersion());
        modelVersionRO.setName(i.getName());
        return ok(new RestResponse<>(modelVersionRO));
    }

    /**
     * Change or set metamodel name for given storage id.
     *
     * @param modelVersionRO the model version RO
     * @return the response
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/" + PATH_PARAM_MODEL_NAME)
    @Operation(
        description = "Меняет имя метамодели для заданого storage id.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = ModelVersionRO.class)), description = "Запрос на вставку"),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response modelName(ModelVersionRO modelVersionRO) {
        name = modelVersionRO.getName();
        return ok(new RestResponse<>(modelVersionRO));
    }

    /**
     * Gets model data associated with optional storage ID. Returns current, if no
     * storage id specified.
     *
     * @param id            storage id.
     * @return character stream
     * @throws Exception the exception
     */
    @GET
    @Path("/export")
    @Produces(MediaType.TEXT_XML)
    @Operation(
        description = "Gets the data model XML by optional storage ID.",
        method = HttpMethod.GET,
        parameters = { @Parameter(description = "Storage ID. Optional", in = ParameterIn.QUERY, name = "storageId") },
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = StreamingOutput.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response dump(@QueryParam("storageId") String storageId) throws UnsupportedEncodingException {

        final DataModelInstance current = StringUtils.isBlank(storageId)
                ? metaModelService.instance(Descriptors.DATA)
                : metaModelService.instance(Descriptors.DATA, storageId, null);

        if (Objects.isNull(current) || current.isEmpty()) {
            return okOrNotFound(null);
        }

        final DataModel model = current.toSource();
        final String encodedFilename = URLEncoder.encode(model.getStorageId() + "_"
                + DateFormatUtils.format(System.currentTimeMillis(), "yyyy-MM-dd_HH-mm-ss") + ".xml",
                StandardCharsets.UTF_8.name());

        final StreamingOutput retval = output -> {
            try {
                output.write(
                    XmlObjectSerializer.getInstance()
                        .toXmlString(model, true)
                        .getBytes(StandardCharsets.UTF_8));
            } catch (Exception e) {
                throw new PlatformBusinessException("Data model marshaling failed.",
                        MetaRestExceptionIds.EX_META_DATA_MARSHALING_FAILED);
            }
        };

        return Response.ok(retval)
                .encoding(StandardCharsets.UTF_8.name())
                .header("Content-Disposition", "attachment; filename=" + model.getStorageId() + ".xml" + "; filename*=UTF-8''" + encodedFilename)
                .header("Content-Type", MediaType.TEXT_XML)
                .build();
    }

    /**
     * Saves binary large object.
     *
     * @param id            golden record id
     * @param recreate the recreate
     * @param attachment            attachment object
     * @return ok/nok
     * @throws Exception the exception
     */
    @POST
    @Consumes(MediaType.MULTIPART_FORM_DATA)
    @Path("/import")
    @Operation(
            description = "Upload data model (partial or full).",
            method = HttpMethod.POST,
            requestBody = @RequestBody(content = @Content(mediaType = MediaType.MULTIPART_FORM_DATA), description = "Request"),
            responses = {
                    @ApiResponse(content = @Content(schema = @Schema(implementation = UpdateResponse.class)), responseCode = "200"),
                    @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
            }
    )
    public Response source(
            @Multipart(value = DATA_PARAM_RECREATE) Boolean recreate,
            @Multipart(value = DATA_PARAM_FILE) Attachment attachment) throws IOException {

        if (!MediaType.TEXT_XML_TYPE.equals(attachment.getContentType())) {
            LOGGER.warn("Invalid content type rejected, while importing model. {}", attachment.getContentType());
            throw new PlatformBusinessException("Import model with the media type [{}] is not allowed.",
                    MetaRestExceptionIds.EX_META_DATA_IMPORT_UNSUPPORTED, attachment.getContentType().toString());
        }

        DataModel model = XmlObjectSerializer.getInstance()
                .fromXmlInputStream(DataModel.class, attachment.getObject(InputStream.class));

        boolean isRecreate = recreate != null && recreate;

        metaModelService.upsert(UpsertDataModelContext.builder()
                .nestedEntitiesUpdate(model.getNestedEntities())
                .lookupEntitiesUpdate(model.getLookupEntities())
                .entitiesGroupsUpdate(model.getEntitiesGroup())
                .entitiesUpdate(model.getEntities())
                .relationsUpdate(model.getRelations())
                .storageId(model.getStorageId())
                .upsertType(isRecreate ? ModelChangeContext.ModelChangeType.FULL : ModelChangeContext.ModelChangeType.MERGE)
                .build());

        return ok(new UpdateResponse(true, model.getStorageId()));
    }

    /**
     * Return dependency graph.
     *
     * @param request
     *            meta elements types.
     * @return dependency graph.
     */
    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    @Path("/" + PATH_PARAM_DEPENDENCY)
    @Operation(
        description = "Возвращает граф зависимостей между выбранными элементами метамодели.",
        method = HttpMethod.POST,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = MetaDependencyRequestRO.class)), description = "Запрос"),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response dependency(MetaDependencyRequestRO request) {
        Set<MetaType> forTypes = MetaTypeROToDTOConverter.convert(request.getForTypes());
        Set<MetaType> skipTypes = MetaTypeROToDTOConverter.convert(request.getSkipTypes());
        String storageId = request.getStorageId();
        MetaGraph result = metaDependencyService.calculateDependencies(storageId, forTypes, skipTypes);
        return ok(new RestResponse<>(MetaGraphDTOToROConverter.convert(result)));
    }
}
